package com.kusiri.kframework.salesforce.model
{
	import com.kusiri.kframework.IKfApplication;
	import com.kusiri.kframework.IKfContext;
	import com.kusiri.kframework.KfApplicationLocator;
	import com.kusiri.kframework.model.KfValueObjectDelta;
	import com.kusiri.kframework.model.KfValueObjectsChangedEvent;
	import com.salesforce.AsyncResponder;
	import com.salesforce.Connection;
	import com.salesforce.objects.LoginRequest;
	import com.salesforce.objects.SObject;
	import com.salesforce.results.DescribeLayoutResult;
	import com.salesforce.results.DescribeSObjectResult;
	import com.salesforce.results.Field;
	import com.salesforce.results.QueryResult;
	import com.salesforce.results.SearchResult;
	
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.collections.ArrayCollection;
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.utils.ObjectProxy;
	import mx.utils.ObjectUtil;
	
	/**
	 * Model for salesforce
	 */
	public class KfSalesforceModel extends EventDispatcher implements IKfSalesforceModel
	{
		/**
		 * 
		 */
		public var sessionId : String;
		
		/**
		 * 
		 */
		public var serverUrl : String;

		/**
		 * 
		 */
		private var self : KfSalesforceModel;


		/**
		 * 
		 */
		private var kfApplication : IKfApplication = new KfApplicationLocator().kfApplication;

		/**
		 * Dictionary, using weak references to avoid memory issues 
		 */
		private var sobjects:Object = new Dictionary(true);
		
		/**
		 * 
		 */
		private var layouts:Object = new Object();
		
		
		/**
		 * What to escape in queries
		 */
		private const RESERVED_CHARACTERS : Array = [
		 '&', '|', '!', '{', '}', '[', ']', '^', '~', ':', '\\', "'", '+', '-'
		 ];
		 
		// '?', '*', '(', ')', '"',  
		 
		public function KfSalesforceModel()
		{
			self = this;
		}		 
		
		private var _name : String;
		
		/**
		 * A user definable name for the component, e.g. "My Table"
		 */
		public function get __name() : String
		{
			return _name;
		}
		
		/**
		 * A user definable name for the component, e.g. "My Table"
		 */
		public function set __name( name : String) : void
		{
			_name = name;
		}
		
		/**
		 * Reg exp to do escaping in queries
		 */
		private var resRegExp:RegExp = new RegExp(
				'('+
					RESERVED_CHARACTERS.map(
						function (element:*, index:int, arr:Array) : Object 
						{
							return '\\'+element;
						}
					,'g').join('|')
				+')');
		
		/**
		 * The salesforce connection
		 */
		protected var connection:Connection = new Connection();
		
		/**
		 * Logger
		 */
		protected var logger:ILogger = Log.getLogger('com.kusiri.salesforce');
		
		/**
		 * The sobjects that we are enabling
		 * TODO: move this into a property for config
		 */
		protected var objectNames:Array = ['CampaignMember','ActivityHistory', 'Task','User','Opportunity', 'Account', 'Contact', 'Lead','Campaign','Event'];
		
		/**
		 * The field names for each object
		 */
		protected var _fieldNames:Object = new Object();
		
		/**
		 * What fields are fetched when an object is requested
		 */
		protected var _fetchedFields:Object = new Object();
		
		/**
		 * Map of object name to DescribeSObjectResult
		 */
		protected var _descriptions:Object = new Object();
		
		// TODO: is this used?
		protected var _relationshipNameToFieldName:Object = new Object();

		/**
		 * Gets a KF description for a type of sobject
		 */
		public function getKfDescriptionForType( type : String ) : XML
		{
			return null;
		}

		/**
		 * Get the deafult(?) lauout for an object type
		 * TODO: Recordtypes
		 */
		public function describeLayout(sobjectType:String, onSuccess:Function, onFail:Function) : void
		{
			if (sobjectType == 'User')
			{
				// TODO: Need to fix this
				onSuccess(new DescribeLayoutResult(new ObjectProxy()));
				return;
			}
			
			var key:String = sobjectType;
			
			if (layouts.hasOwnProperty(key))
			{
				onSuccess(layouts[key]);
				return;
			}
			
			connection.describeLayout(sobjectType, null,
					new AsyncResponder(
						function(o:DescribeLayoutResult):void 
						{ 
							layouts[key] = o; 
							onSuccess(o); 
						},
						function(o:Object):void { log(o); onFail(o); }
					)
				);
		}

		/**
		 * Gets the salesforce user id of the current user
		 */
		public function get __currentUserId() : String
		{
			return connection.loginResult.userId;
		}

		/**
		 * TODO: is this used?
		 */
		public function relationshipNameToFieldName(relationshipName:String) : String
		{
			return _relationshipNameToFieldName[relationshipName];
		}
		
		/**
		 * Escape a string for querying
		 */
		public function escape(string:String) : String
		{
			return string.replace(resRegExp, "\\$1");
		}
		
		/**
		 * Get SOQL (minus the WHERE clause) to select an object
		 */
		public function getSoqlToSelect(sobjectType : String) :String
		{
			
			return "SELECT "+
				
				_fetchedFields[sobjectType].join(', ')
				
				+" FROM "+sobjectType;
		}
		
		/**
		 * Get an sobject from the cache
		 */
		public function getFromCache(id : String) : ISObject
		{
			return sobjects[id];
		}
		
		/**
		 * Set an sobject in the cache
		 */
		public function setInCache(id : String, sobject : ISObject) : void
		{
			sobjects[id] = sobject;
		}
		
		/**
		 * Get all the DescribeSObjectResults
		 */
		public function get __descriptions() : Object
		{
			return _descriptions;
		}
		
		/**
		 * Get the map of object name => field names
		 */
		public function get __fieldNames() : Object
		{
			return _fieldNames;
		}
		
		/**
		 * Search for sobjects
		 */
		public function findSObjects(keywords:String, sobjectType:String, onSuccess:Function, onFail:Function) : void
		{
			var q:String = "find {"+escape(keywords)+"} " + 
					        "in "+sobjectType+" fields returning " +
					        sobjectType+"("+_fetchedFields[sobjectType]+")";
					        
			logger.debug(q);
			
			kfApplication.waitingForSomething();
			
			connection.search(q,
				new AsyncResponder(
					function(o:Object) : void
					{
						kfApplication.finishedWaitingForSomething();
						log(o);
						var sr:SearchResult = SearchResult(o);
						onSuccess(convertSObjects(sobjectType, sr.searchRecords, true));
					},
					function(o:Object) : void
					{
						kfApplication.finishedWaitingForSomething();
						log(o);
						onFail(o);
					}
				)
			);
		}
		
		/**
		 * Log an object
		 */
		private function log(o:*) : void
		{
			logger.debug(ObjectUtil.toString(o));
		}
		
		/**
		 * Get an Sobject
		 */
		public function getSObject(sobjectType:String, Id:String, onSuccess:Function, onFail:Function, forceReload:Boolean = false) : void
		{
			// in cache?
			if ( ! forceReload && sobjects[Id] && !sobjects[Id].stub)
			{
				onSuccess(sobjects[Id]);
				return;
			}
			
			// no placeholders?			
			var query:String = getSoqlToSelect(sobjectType)+" WHERE Id='"+escape(Id)+"'";
			logger.debug(query);
			
			kfApplication.waitingForSomething();
			
			connection.query(
				query , 
				new AsyncResponder( 
					function(qr:QueryResult) : void 
					{ 
						kfApplication.finishedWaitingForSomething();
						log(qr);
						var ac:ArrayCollection = convertSObjects(sobjectType, qr.records);
						onSuccess(ac.length > 0 ? ac.getItemAt(0) : null)
	  				}
	        		, 
	        		function(o:Object) : void
					{
						kfApplication.finishedWaitingForSomething();
						log(o);
						onFail(o);
					}
				)
			);

	 	}		
	 	
	 	/**
	 	 * Special function to get activity history, as it's not a standard object
	 	 */
	 	public function getActivityHistory(sobject:com.kusiri.kframework.salesforce.model.ISObject, onSuccess:Function, onFail:Function) : void
	 	{
	 		var type : String = sobject.__type;
	 		var query : String = "SELECT (SELECT "+_fetchedFields['ActivityHistory'].join(', ')+" from ActivityHistories) FROM "+type+" WHERE Id='"+escape(sobject.Id)+"'";
	 		
	 		kfApplication.waitingForSomething();
	 		
	 		connection.query(
				query , 
				new AsyncResponder( 
					function(qr:QueryResult) : void 
					{ 
						kfApplication.finishedWaitingForSomething();
						log(qr);
						if (!qr.records[0]) 
						{
							onSuccess(new ArrayCollection());
							return;
						}
						var result : ArrayCollection = qr.records[0].ActivityHistories.records;
						onSuccess(  convertSObjects('ActivityHistory', result));
	  				}
	        		, 
	        		function(o:Object) : void
					{
						kfApplication.finishedWaitingForSomething();
						log(o);
						onFail(o);
					}
	        	)
			);
		}
	 	
	 	/**
	 	 * Get a selection of sobjects of a single type
	 	 */
	 	public function getSObjects(sobjectType:String, queryCondition:String, onSuccess:Function, onFail:Function) : void
	 	{
	 		var query : String = getSoqlToSelect(sobjectType)+' '+queryCondition;
	 		
	 		kfApplication.waitingForSomething();
	 		
	 		connection.query(
				query , 
				new AsyncResponder( 
					function(qr:QueryResult) : void 
					{ 
						var result:ArrayCollection = convertSObjects('Task', qr.records);
						log(qr);
						kfApplication.finishedWaitingForSomething();
						onSuccess(result);
	  				}
	        		, 
	        		function(o:Object) : void
					{
						kfApplication.finishedWaitingForSomething();
						log(o);
						onFail(o);
					}
	        	)
			);
		}
		
		/**
		 * Tell the Component to get ready to be used
		 */
		public function __initializeComponent(onInitialized : Function, onFail : Function) : void
		{
			var lr:LoginRequest  = new LoginRequest();
			
			lr.session_id = sessionId;
			lr.server_url = serverUrl;
			
			lr.callback = new AsyncResponder(
				function (o:Object) : void 
				{
					_loadMetadata(objectNames, onInitialized, onFail);
				},
				function (error:*) : void
				{
					onFail(self, error);
				});
				
			connection.protocol = 'https';
			connection.login(lr);

		}
		
		/**
		 * All objects in salesforce
		 */
		public function getAllObjectNames(onSuccess: Function, onFail: Function) : void
		{			

			connection.describeGlobal(
				new AsyncResponder(
					function(r:Object) : void
					{
						onSuccess(r.types.source);
					}
					,
					function(fault:Object) : void
					{
						onFail(fault);
					}
				)
			);
		}

		/**
		 * 
		 */
		public function objectNameToLabel(name:String) : String
		{
			var o:DescribeSObjectResult = __descriptions[name];
			return o.label;
		}
		
		/**
		 * 
		 */
		public function objectNamesToLabels(names:Array) : Array
		{
			return names.map(
				function (element:*, index:int, arr:Array) : Object 
				{
					var o:DescribeSObjectResult = __descriptions[element];
					return o.label;
				}
			);
		}
				
		/**
		 * Load salesforce meta data about objects
		 */		
		protected function _loadMetadata(sobjectTypes : Array, onSuccess: Function, onFail: Function) : void
		{	
				
			kfApplication.waitingForSomething();
				
			connection.describeSObjects(sobjectTypes,
				new AsyncResponder(
					function (a:Array) : void
					{
					
						kfApplication.finishedWaitingForSomething();
										
						_descriptions = new Object();
						_fieldNames = new Object();
						
						for(var i:int=0, j:int=a.length; i<j; i++)
						{
							var result:DescribeSObjectResult = a[i];
							_descriptions[result.name] = result;
							var fieldNames:Array = [];
							var fetchedFields:Array = [];
							for (var fieldName:String in result.fields)
							{
								var field:Field = result.fields[fieldName];
								if (field.type == 'reference')
								{
									var relationshipName:String = field.relationshipName;
									
									_relationshipNameToFieldName[relationshipName] = fieldName;
									
									// if no relationship name prolly deprecated
									if (!relationshipName) continue;
									
									fieldNames.push(fieldName);	
									fetchedFields.push(fieldName);
									
									var referenceTo:Array = field.referenceTo.split(',');
									if (referenceTo.length == 1)
									{
									
										// direct link to object
										var sobjectType:String = referenceTo[0];										
									
										// always need the id 
										fetchedFields.push(relationshipName+'.Id');
										
										if (sobjectType == 'User')
										{
											fetchedFields.push(relationshipName+'.FirstName');
											fetchedFields.push(relationshipName+'.LastName');
										}
										else
										{
											fetchedFields.push(relationshipName+'.Name');
										}
									}
									else // multi-reference
									{
										fetchedFields.push(relationshipName+'.Id');
										fetchedFields.push(relationshipName+'.Name');
										fetchedFields.push(relationshipName+'.Alias');
										fetchedFields.push(relationshipName+'.FirstName');
										fetchedFields.push(relationshipName+'.LastName');
										fetchedFields.push(relationshipName+'.Type');
									}
								}
								else // not a reference
								{
									fieldNames.push(fieldName);	
									fetchedFields.push(fieldName);
								}
							}
							
							// got all the fields for a soql select now
							_fieldNames[result.name] = fieldNames;
							_fetchedFields[result.name] = fetchedFields;
						}
						
						onSuccess(self);
					},
	        		function(o:Object) : void
					{
						kfApplication.finishedWaitingForSomething();
						log(o);
						onFail(o);
					}
				)
			);
		}		
		
		/**
		 * convert standard SObjects to specific Sobjects
		 */
		private function convertSObjects(sobjectType:String, array:ArrayCollection, searchResult:Boolean=false) : ArrayCollection
		{
			if (array==null) return new ArrayCollection();
			
			var result:ArrayCollection = new ArrayCollection(
							array.source.map(
								function (element:*, index:int, arr:Array) : Object 
								{
									if (searchResult) element = element.record;



									if (sobjects.hasOwnProperty(element.Id))
									{
										sobjects[element.Id].__proxiedObject = element;
									}
									else
									{
	            						var sobject:com.kusiri.kframework.salesforce.model.SObject;
	            						sobject = new com.kusiri.kframework.salesforce.model.SObject();
	            						sobject.__initialize(element, self);
            							sobjects[element.Id] = sobject
         							}
            						
            						return sobjects[element.Id];
        						}
        					)
        				);
        				
        	return result;
		}
		
		/**
		 * onSuccess takes the salesforce id of the object
		 */
		public function deleteObject(sobject:com.kusiri.kframework.salesforce.model.ISObject, onSuccess:Function = null, onFail:Function = null) : void
		{
        	deleteObjects([sobject], function(ids:Array) : void { onSuccess(sobject); }, onFail);
	 	}	

		/**
		 * onSuccess takes the salesforce ids Array of the objects
		 */
		public function deleteObjects(valueObjects:Array, onSuccess:Function = null, onFail:Function = null) : void
		{
			kfApplication.waitingForSomething();
			
        	connection.deleteIds(
        		valueObjects.map(
        			function (vo:*, index:int, arr:Array) : String { return vo.Id }
        		),
				new AsyncResponder(
					function (o:Object) :void 
				    {  
				    	kfApplication.finishedWaitingForSomething();
				    	log(o);
				    	
				    	valueObjects.forEach(
				    		function (vo:*, index:int, arr:Array) : void 
							{
				    			delete sobjects[vo.Id];
				   			}
				    	);
				    	
				    	var event : KfValueObjectsChangedEvent = new KfValueObjectsChangedEvent();
				    	event.deletedObjects = valueObjects;
				    	dispatchEvent(event);
				    	
				    	if (onSuccess != null) onSuccess(valueObjects);
         			},
         			function (o:Object) :void 
				    {  
				    	kfApplication.finishedWaitingForSomething();
						logger.debug('Save fail: '+ObjectUtil.toString(o));
				    	if (onFail != null) onFail(o);
         			}
         	));
	 	}	

		/**
		 * This will update the kusiri model copy
		 * 
		 * TODO: change this to take a simple value object + type, not a sf sobject
		 * 
		 * onSuccess takes the kusiri sobject
		 */
	 	public function saveObject(sobjectType:String, pofo:Object, onSuccess:Function = null, onFail:Function = null) : void
	 	{
	 		
	 		var sobject:com.salesforce.objects.SObject = new com.salesforce.objects.SObject(sobjectType);
	 		
	 		var isUpdate:Boolean = pofo.Id != null;
	 		
		    var lamb:Function = isUpdate ? connection.update : connection.create;

			kfApplication.waitingForSomething();

			sobject.fieldsToNull = [];
			for (var prop:String in pofo )
			{
				if ( pofo[prop] == null ) //&& sobject[prop] !== false) 	    
				{
					if (isUpdate) sobject.fieldsToNull.push(prop);
				}
				else
				{
					sobject[prop] = pofo[prop];
				} 	
			}

			// TODO: Connection not reporting failed saves! Need to fix this

			lamb(
				[sobject],
				new AsyncResponder(
				
				    function (o:Object) :void 
				    {  
				    	kfApplication.finishedWaitingForSomething();
				    	
				    	log(o);
				    	
				    	var result : Object = o[0];
				    	
				    	if (result.success)
				    	{
				    	
							// we have a new id
							if (!isUpdate) 
							{
								sobject.Id = result.id;
							}
							
							// refresh object 
							var currentSObject : com.kusiri.kframework.salesforce.model.SObject = (sobjects[sobject.Id] as com.kusiri.kframework.salesforce.model.SObject);
							var oldSObject : com.kusiri.kframework.salesforce.model.SObject = currentSObject == null ? null : currentSObject.__clone();
							var oldProxiedObject : Object = oldSObject == null ? {} : oldSObject.__proxiedObject;
							
							getSObject(sobject.type, sobject.Id, 
								function(sobject:com.kusiri.kframework.salesforce.model.SObject) : void
								{
									
									var event : KfValueObjectsChangedEvent = new KfValueObjectsChangedEvent();
									
									if ( isUpdate ) 
									{
										var newProxiedObject : Object = sobject.__proxiedObject;
										var changedFields : Object = {};
										for ( var prop : String in newProxiedObject )
										{
											if (oldProxiedObject[prop] is com.salesforce.objects.SObject && newProxiedObject[prop] is com.salesforce.objects.SObject)
											{
												if (oldProxiedObject[prop].Id!=newProxiedObject[prop].Id)
												{
													changedFields[prop] = oldProxiedObject[prop];
												}
											}
											else if (oldProxiedObject[prop]!=newProxiedObject[prop])
											{
												changedFields[prop] = oldProxiedObject[prop];
											}
										}
										var delta : KfValueObjectDelta = new KfValueObjectDelta();
										delta.changedFields = changedFields;
										delta.valueObject = sobject;
										delta.oldValueObject = oldSObject;
								    	event.updatedObjects = [delta];
									}
									else
									{	
								    	event.createdObjects = [sobject];
									}
									
									dispatchEvent(event);
									
									if (onSuccess is Function) onSuccess(sobject);
								}
							, onFail, true);
				    	}
				    	else // not successful
				    	{
				    		// array collection of com.salesforce.results.Error
				    		onFail(result.errors);
				    	}
				    	
						
         			},
         			function (o:Object) :void 
				    {  
				    	kfApplication.finishedWaitingForSomething();
				    	
						logger.debug('Save fail: '+ObjectUtil.toString(o));
				    	if (onFail != null) onFail(o);
         			}
         			
         		)
			);
				 		
	 	}


	}
}